﻿//Copyright (c) 2008 Michael J. Ryan & Apollo Group, Inc.

//Permission is hereby granted, free of charge, to any person
//obtaining a copy of this software and associated documentation
//files (the "Software"), to deal in the Software without
//restriction, including without limitation the rights to use,
//copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the
//Software is furnished to do so, subject to the following
//conditions:

//The above copyright notice and this permission notice shall be
//included in all copies or substantial portions of the Software.

//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
//EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
//OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
//HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
//WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
//OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Web;
using System.Configuration;
using System.IO;
using System.Text.RegularExpressions;

namespace Apollo.Common.Configuration
{
	internal class ConfigurationCache
	{
#region Private Member Variables
		//NOTE: the only place enumerations agaist the _cache should be used is inside CacheUpdater
		//			enumerations "foreach", "select" and other similar uses are *NOT* thread-safe.
		//			looking up on the key is okay.
		private Dictionary<string, ConfigurationFile> _cache;
		private readonly Queue<ConfigurationFile> _cacheQueue;
		private readonly Core.Environment _env;
		private readonly string _app;
		private readonly string _ver;
		private readonly string _basepath;
		
		//cache updater related.
		private object _updateStarterLock = new Object();
		private Thread _configFileUpdater;
		private DateTime _cacheUpdated = DateTime.MinValue;
#endregion
	
#region Constructor(s)
		internal ConfigurationCache()
		{
			 _cache = new Dictionary<string,ConfigurationFile>();
			 _cacheQueue = new Queue<ConfigurationFile>();
			 GetAppSettings(out _basepath, out _app, out _ver, out _env);
			 StartCacheUpdater();
		}
#endregion

#region Public Properties

	#region ApplicationName
		public string ApplicationName
		{
			get { return _app; }
		}
	#endregion
	
	#region ApplicationVersion
		public string ApplicationVersion
		{
			get { return _ver; }
		}
	#endregion
	
	#region ApplicationEnvironment
		public Core.Environment ApplicationEnvironment
		{
			get { return _env; }
		}
	#endregion
	
	#region BaseConfigurationPath
		public string BaseConfigurationPath
		{
			get { return _basepath; }
		}
	#endregion

#endregion

#region Public Methods
	
	#region GetConfigurationFile
		public ConfigurationFile GetConfigurationFile(string settingName)
		{
			ConfigurationFile f; //for return value
			string s = settingName.Trim().ToLower();
			
			//try-load from cache
			if (_cache.TryGetValue(s, out f))
				return f;
			
			//check the cachequeue.
			lock (_cacheQueue) {
				f = _cacheQueue.SingleOrDefault(cf => cf.Name == s);
				if (f != null)
					return f; //use already added item.
				
				//no item found, add one.
				f = new ConfigurationFile(s, GetFileInfo);
				_cacheQueue.Enqueue(f);
				return f;
			}
		}
	#endregion
	
	#region ClearCache
		public void ClearCache()
		{
			lock(_cacheQueue) {
				_cacheQueue.Clear(); //clear queue
				_cache = new Dictionary<string,ConfigurationFile>(); //empty cache
			}
		}
	#endregion
	
#endregion

#region Internal Methods
	
	#region GetFileInfo
		/// <summary>
		/// Get the configuration file for a given setting within the application
		/// </summary>
		/// <param name="settingName">Common name of the setting file to find.</param>
		/// <returns>FileInfo for the settings file in question.</returns>
		internal FileInfo GetFileInfo(string settingName)
		{
			//filesystem
			Queue<string> searchpath = GetEndpointPaths(_basepath, _env, _app, _ver, settingName);
			foreach (string s in searchpath) {
				if (File.Exists(s))
					return new FileInfo(s);
			}
			return null;
		}
	#endregion
	
#endregion

#region Private Methods
	
	#region GetAppSettings
		private void GetAppSettings(out string basepath, out string applicationName, out string applicationVersion, out Core.Environment environment)
		{
			GetAppBasepath(out basepath);
			GetAppName(out applicationName);
			GetAppVersion(out applicationVersion);
			GetAppEnvironment(out environment);

		}

		private void GetAppBasepath(out string basepath)
		{
			try
			{
				string bp = ConfigurationSettings.AppSettings["acc-endpoint"] ?? "";
				if (string.IsNullOrEmpty(_basepath)) {
					LogTool.Logger.Warn("Apollo...ConfigurationClient.GetAppSettings(): no 'acc-endpoint' in the application/web config file.");
					bp = GetDefaultEndPointPattern();
				}
				basepath = bp.Trim();
			}
			catch (Exception ex)
			{
				LogTool.Logger.ErrorException("Apollo...ConfigurationClient.GetAppSettings(), error retrieving application setting 'acc-application-name'.", ex);
				throw;
			}
		}
		
		private void GetAppName(out string applicationName)
		{
			try
			{
				applicationName = ConfigurationSettings.AppSettings["acc-application-name"] ?? "";
				if (string.IsNullOrEmpty(applicationName))
				{
					LogTool.Logger.Warn("Apollo...ConfigurationClient.GetAppSettings(): no 'acc-application-name' in the application/web config file.");
				}
			}
			catch (Exception ex)
			{
				LogTool.Logger.ErrorException("Apollo...ConfigurationClient.GetAppSettings(), error retrieving application setting 'acc-application-name'.", ex);
				throw;
			}

		}
		
		private void GetAppVersion(out string applicationVersion)
		{
			try
			{
				applicationVersion = ConfigurationSettings.AppSettings["acc-application-ver"] ?? "";
				if (string.IsNullOrEmpty(applicationVersion))
				{
					LogTool.Logger.Warn("Apollo...ConfigurationClient.GetAppSettings(): no 'acc-application-ver' in the application/web config file.");
				}
			}
			catch (Exception ex)
			{
				LogTool.Logger.ErrorException("Apollo...ConfigurationClient.GetAppSettings(), error retrieving application setting 'acc-application-ver'.", ex);
				throw;
			}
		}
		
		private void GetAppEnvironment(out Core.Environment environment)
		{
			string envs;
			
			//load from app/web config
			try
			{
				envs = ConfigurationSettings.AppSettings["acc-enviroment"] ?? "";
				if (string.IsNullOrEmpty(envs))
				{
					LogTool.Logger.Warn("Apollo...ConfigurationClient.GetAppSettings(): no 'acc-enviroment' in the application/web config file.");
				}
			}
			catch (Exception ex)
			{
				LogTool.Logger.ErrorException("Apollo...ConfigurationClient.GetAppSettings(), error retrieving application setting ''.", ex);
				throw;
			}
			
			try
			{
				//if empty try machine/dns name.
				if (string.IsNullOrEmpty(envs))
					envs = GetEnvironmentFromMachine();					

				//if empty, try from a web request
				if (string.IsNullOrEmpty(envs))
					envs = GetEnvironmentFromHttpContext();
					
				if (!string.IsNullOrEmpty(envs)) {
					//if matching, use existing match.
					environment = (Core.Environment)Enum.Parse(typeof(Core.Environment), envs.ToUpper());
				} else {
#if DEBUG
					environment = Core.Environment.LOCAL; //local is default for debug.
#else
					environment = Core.Environment.PRODUCTION; //production is default for non-debug
#endif
				}
					
			}
			catch (Exception ex)
			{
				LogTool.Logger.ErrorException("Apollo...ConfigurationClient.GetAppSettings(), error parsing application setting 'acc-enviroment'.", ex);
				throw;
			}
		}
		
		private string GetEnvironmentFromMachine()
		{
			string host = string.Empty;
			
			//get hostname from the environment.        
			try  {	        
				host = System.Net.Dns.GetHostName().ToLower();
			} catch (Exception) {}
			
			//if no dns lookup for hostname, use machinename.
			if (string.IsNullOrEmpty(host)) {
				try {
					host = Environment.MachineName.ToLower();
				}
				catch (Exception) {}
			}
			
			return GetEnvironmentFromHostName(host);
		}
		
		private string GetEnvironmentFromHttpContext()
		{
			if (HttpContext.Current != null)
				return GetEnvironmentFromHostName(HttpContext.Current.Request.Url.Host);
			return String.Empty;
		}
		
		private string GetEnvironmentFromHostName(string hostName)
		{				
			//local
			if (hostName == "localhost")
				return Core.Environment.LOCAL.ToString();
				
			//dev
			if (hostName.IndexOf("dev") == 0 || hostName.IndexOf(".dev") >= 0)
				return Core.Environment.DEVELOPMENT.ToString();
			
			//qa
			if (hostName.IndexOf("qa") == 0 || hostName.IndexOf(".qa") >= 0)
				return Core.Environment.QA.ToString();
				
			//no match
			return String.Empty; 
		}
	#endregion
	
	#region EndpointPatternFix
		private string EndpointPatternFix(Match m)
		{
			switch (m.Value.ToLowerInvariant()) {
				case "{env}":
					return "{0}";
				case "{app}":
					return "{1}";
				case "{ver}":
					return "{2}";
				case "{cfg}":
					return "{3}";
			}
			return m.Value;
		}
	#endregion
	
	#region GetEndpointPaths
		private Queue<string> GetEndpointPaths(string pathPattern, Core.Environment environment, string applicationName, string applicationVersion, string settingName)
		{
			Queue<string> ret = new Queue<string>();
		
			//change basepath to have string.format parms
			//		{env} {0},	{app} {1},	{ver} {2},	{cfg} {3},
			string basepath = Regex.Replace(pathPattern, "{[^}]+}", new MatchEvaluator(EndpointPatternFix), RegexOptions.IgnoreCase);
			
			//full
			ret.Enqueue(GetEndpointPath(basepath, environment, applicationVersion, applicationVersion, settingName));
			
			//no ver
			ret.Enqueue(GetEndpointPath(basepath, environment, applicationVersion, "", settingName));
			
			//no ver/app
			ret.Enqueue(GetEndpointPath(basepath, environment, "", "", settingName));
			
			//no env/ver/app
			ret.Enqueue(GetEndpointPath(basepath, null, "", "", settingName));
			
			return ret;
		}
		
		private string GetEndpointPath(string pathPattern, Core.Environment? environment, string applicationName, string applicationVersion, string settingName)
		{
			string ret = string.Format(
				pathPattern, 
				(environment == null) ? "{env}" : environment.ToString().ToLower(), 
				applicationName.Trim().ToLower().Replace(" ", "_"),
				applicationVersion.Trim().ToLower().Replace(" ", "_"),
				settingName.Trim().ToLower().Replace(" ", "_")
			);
			
			ret = Regex.Replace(ret, @"\.+", @"."); //reduce multiple periods
			ret = Regex.Replace(ret, @"\\+", "\\"); //reduce multiple backslashes
			ret = Regex.Replace(ret, @"\/+", "/"); //reduce multiple forward slashes.
			if (environment == null)
			{
				//remove default from environment.
				ret = ret.Replace("{env}.", "");
				ret = ret.Replace("{env}\\", "");
				ret = ret.Replace("{env}/", "");
			}
			return ret;
		}
	#endregion
	
	#region GetDefaultEndPointPattern
		public string GetDefaultEndPointPattern() {
			try
			{
				string endpointdir = "Apollo.Common.Configuration";
	
				Queue<string> searchpath = new Queue<string>();
				GetDefaultEndPointPatternWeb(ref searchpath);
				GetDefaultEndPointPattern(ref searchpath);

				string dir;
				string dir2;
				while (!string.IsNullOrEmpty(dir = searchpath.Dequeue()))
				{
					try
					{
						dir2 = Path.Combine(dir, endpointdir);
						if (Directory.Exists(dir2))
							return Path.Combine(dir2, "{cfg}.{env}.{app}.{ver}.config");
					}
					catch (Exception ex)
					{
						LogTool.Logger.WarnException(string.Format("Apollo...ConfigurationClient.DefaultEndPointPattern: exception testing '{0}'.", dir), ex);
					}
				}

				return "";
			}
			catch (Exception ex)
			{
				LogTool.Logger.FatalException("Apollo...ConfigurationClient.DefaultEndPointPattern: Major exception searching for default config path.", ex);
				throw;
			}
		}
	
		private void GetDefaultEndPointPattern(ref Queue<string> searchpath)
		{
			string dir;
			DirectoryInfo di;

			//root directory
			try
			{
				//Get system's root.
				if (Path.DirectorySeparatorChar.Equals('\\')) {
					//windows
					dir = System.Environment.GetFolderPath(Environment.SpecialFolder.System);
					di = new DirectoryInfo(dir);
					while (di.Parent != null) di = di.Parent; //goto root...
					searchpath.Enqueue(di.FullName);
					
					if (di.FullName.Length > 0 && di.FullName.ToLower()[0] != 'c')
						searchpath.Enqueue("c:\\"); //add C: to search path, if not the system's root drive
				} else {
					// *nix mono
					searchpath.Enqueue("/");
					searchpath.Enqueue("/etc");
				}
			}
			catch (Exception ex)
			{
				LogTool.Logger.WarnException("Apollo...ConfigurationClient.GetDefaultEndPointPattern(): Error Retreiving root", ex);
			}

			//all users appdata
			try
			{
				//test for {AllUsers\AppData}\Apollo.Common.Configuration
				searchpath.Enqueue(System.Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData));
			}
			catch (Exception ex)
			{
				LogTool.Logger.WarnException("Apollo...ConfigurationClient.GetDefaultEndPointPattern(): Error Retreiving CommonApplicationData Directory", ex);
			}

			//system dir (windows only, C:\Windows\)
			try
			{
				if (Path.DirectorySeparatorChar.Equals('\\'))
				{
					//system dir
					dir = System.Environment.GetFolderPath(Environment.SpecialFolder.System);
					searchpath.Enqueue(dir);
				}
			}
			catch (Exception ex)
			{
				LogTool.Logger.WarnException("Apollo...ConfigurationClient.GetDefaultEndPointPattern(): Error Retreiving System Directory", ex);
			}
			
		}
		
		private void GetDefaultEndPointPatternWeb(ref Queue<string> searchpath)
		{
			if (HttpContext.Current != null) {
				try
				{
					searchpath.Enqueue(HttpContext.Current.Server.MapPath("~/App_Data"));
					searchpath.Enqueue(HttpContext.Current.Server.MapPath("~/App_Config"));
					searchpath.Enqueue(HttpContext.Current.Server.MapPath("~/Config"));
					searchpath.Enqueue(HttpContext.Current.Server.MapPath("~/"));
				}
				catch (Exception ex)
				{
					LogTool.Logger.WarnException("Apollo...ConfigurationClient.GetDefaultEndPointPatternWeb: Exception with mappath.", ex);					
					throw;
				}
			}
		}
	#endregion		
#endregion

#region Cache
		
	#region StartCacheUpdater - create/start the CacheUpdater thread.
		private void StartCacheUpdater() {
			lock(_updateStarterLock) {
				try
				{
					if (_configFileUpdater != null && _configFileUpdater.IsAlive)
						_configFileUpdater.Abort(); //stop running process
						
					Thread.Sleep(1000); //wait a second for the prior thread to clear out.
						
					_configFileUpdater = new Thread(new ThreadStart(CacheUpdater));
					_configFileUpdater.Start();
				}
				catch (ThreadAbortException)
				{
					LogTool.Logger.Info("Apollo...ConfigurationCache.StartCacheUpdater: Shutting down the static thread.");
				}
				catch (Exception ex)
				{
					LogTool.Logger.FatalException("Apollo...ConfigurationCache.StartCacheUpdater: Raised a fatal exception.", ex);
					throw;
				}
			}
		}
	#endregion
	
	#region CacheUpdater - main cache updater thread.
		//updates the pending cache items
		private void CacheUpdater() {
			try
			{
				Thread.Sleep(1000); //wait 1 second to start running.
			
				while(true) {
					try
					{
						Thread.Sleep(500); //wait 1/2 second each iteration
						
						lock(_cacheQueue) {
							//if there are updates to role, or it's been an hour since the last rollup.
							if (_cacheQueue.Count > 0 || (_cacheUpdated < DateTime.Now.AddHours(-1) && _cache.Count > 0))
							{
								//update the filecache.
								_cacheUpdated = DateTime.Now;
								
								Dictionary<string, ConfigurationFile> tmp = new Dictionary<string,ConfigurationFile>();
								string[] keys = _cache.Keys.ToArray();
								foreach(KeyValuePair<string, ConfigurationFile> kv in _cache) {
									if (!kv.Value.IsStale)
										tmp.Add(kv.Key, kv.Value);
								}
								
								//dequeue the waiting stack
								ConfigurationFile cf;
								while (_cacheQueue.Count > 0) {
									cf = _cacheQueue.Dequeue();
									if (!tmp.ContainsKey(cf.Name))
										tmp.Add(cf.Name, cf);
								}
								
								_cache = tmp; //replace _cache reference pointer in one call
							}
						}
					}
					catch (ThreadAbortException)
					{
						//closing the thread
						LogTool.Logger.Info("Apollo...ConfigurationCache.FileCacheUpdater: Shutting down the update thread.");
						return;
					}
				}
			}
			catch (Exception ex)
			{
				LogTool.Logger.ErrorException("Apollo...ConfigurationCache.FileCacheUpdater(), exception occurred, recovering", ex);
				StartCacheUpdater(); //will restart updater thread.
			}
		}
	#endregion
#endregion

	}
}
